package tileStyle;

import java.util.*;
import tileStyle.Prism.*;
import tileStyle.Prism.events.*;
import Prism.core.*;
import Prism.extensions.port.distribution.*;
import Prism.extensions.port.*;
import java.io.*;

/**
 * @author Yuriy
 * 
 * A Node represents a node on a tile style network.   
 * It can deploy tiles.    
 * A TileType is mutable.
 * 
 * @param TileType type    : the type of tiles this node can deploy 
 *                               (used for computing and for input seeds).
 * @param List<Tile> tiles : the tiles deployed on this Node.
 * @param Map<TileType, List<Node>> lookup : a lookup table of where each type of a tile 
 *                                           is deployed.
 */
public class Node extends TileStyleComponent {
	
	public static final long serialVersionUID = 1L;
//	public static String NAME = "Component";
	
	protected NodeAddress address;
	
	protected TileType type;
	protected List<Tile> tiles;
	protected Map<TileType, List<Port>> typeLookup;
	protected Map<Integer, Port> addressLookup;
	protected Set<Port> seedPorts;
	
	protected Architecture architecture;
//	protected Scaffold scaffold;
	protected SocketDistribution sd;
	protected FIFOScheduler scheduler;
	protected RRobinDispatcher dispatcher;
	
	protected ExtensiblePort inPort;
	
	protected BufferedWriter outFile;
	
	private Node(String name) {
		super(name);
//		request = new Port("incomming", PrismConstants.REQUEST);
//		reply = new Port("incomming", PrismConstants.REPLY);
	}

	/*
	 * @param String name : the name of this tile.
	 * @param AbstractImplementation implementation : an implementation
	 * Creates a new Tile with the given name and implementation.  
	 * This constructor is only here for the superclass.
	 */
//	private Node(String name, AbstractImplementation implementation) {
//		super(name, implementation);
//		request = new Port("incomming", PrismConstants.REQUEST);
//		reply = new Port("incomming", PrismConstants.REPLY);
//	}
	
	private void initializeArchitecture(int portNum) {
		architecture = new Architecture("Node");
		scheduler = new FIFOScheduler();
		Scaffold scaff = new Scaffold();
		dispatcher = new RRobinDispatcher(scheduler, 10);
		
		scaff.dispatcher = dispatcher;
		scaff.scheduler = scheduler;
		scaffold = scaff;

		architecture.scaffold = scaffold;
		
		inPort = new ExtensiblePort("inPort" + portNum, PrismConstants.REPLY);
		sd = new SocketDistribution(inPort, portNum);
		inPort.addDistributionModule(sd);
		inPort.scaffold = scaffold;
		this.addCompPort(inPort);
		architecture.add(inPort);
		architecture.add(this);
		
		dispatcher.start();
		architecture.start();
	}
	
	public void createLookup(Map<TileType, List<NodeAddress>> addresses) throws IOException {
		typeLookup = Collections.synchronizedMap(new HashMap<TileType, List<Port>>());
		addressLookup = Collections.synchronizedMap(new HashMap<Integer, Port>());
		seedPorts = new HashSet<Port>();
		
//		synchronized(addresses) {
		for (Iterator<TileType> types = addresses.keySet().iterator(); types.hasNext();) {
			TileType currentType = types.next();
			List<Port> ports = new ArrayList<Port>();
			for (Iterator<NodeAddress> currentAddresses = addresses.get(currentType).iterator(); currentAddresses.hasNext();) {
				NodeAddress currentAddress = currentAddresses.next();
				Port currentPort = null;
				if (currentAddress.isOnHost(address.getHostname())) {
					currentPort = new Port("out node local port", PrismConstants.REQUEST);
				}
				else {
					ExtensiblePort currentExtensiblePort = new ExtensiblePort("out node port", PrismConstants.REQUEST);
					SocketDistribution currentSD = new SocketDistribution(currentExtensiblePort);   
					currentExtensiblePort.addDistributionModule(currentSD);
					currentPort = currentExtensiblePort;
				}
				currentPort.scaffold = scaffold;
				this.addCompPort(currentPort);
				architecture.add(currentPort);
				ports.add(currentPort);

				if (currentPort instanceof ExtensiblePort) {
					if (TileStyleStarter.DEBUG)
						System.out.println("connecting to:" + currentAddress.getHostname() + ":" + currentAddress.getPort());
					((ExtensiblePort) currentPort).connect(currentAddress.getHostname(), currentAddress.getPort());

/*                  boolean didNotConnect = true;
 					while(didNotConnect) {
						try {
							didNotConnect = false;
							((ExtensiblePort) currentPort).connect(currentAddress.getHostname(), currentAddress.getPort());
						} catch (IOException e) {
							didNotConnect = true;
						}
					}
*/				}
				else {
//					System.out.println("connecting locally");
					architecture.weld(currentPort, currentAddress.getNode().getNewPort());
				}
				currentPort.start();

				if (currentAddress.isForSeed())
					seedPorts.add(currentPort);
				addressLookup.put(currentAddress.getUniqueID(), currentPort);
			}
			typeLookup.put(currentType, ports);
//			}
		}
	}

	/*
 	 * @param String name : this Node's name
	 * @param TileType compute, in : the types of tiles this Node can deploy
	 * @param Map<TileType, List<Node>> lookup : the lookup map
	 * Creates a new Node
	 */
	public Node(String name, TileType type, String hostname, int uniqueID, int port, BufferedWriter outFile) {
		this(name);
		this.type = type;
		this.outFile = outFile;
		tiles = Collections.synchronizedList(new ArrayList<Tile>());
		initializeArchitecture(port);
		address = new NodeAddress(hostname, uniqueID, port);
	}
	
	/*	 
	 * @param String name : this Node's name
	 * @param AbstractImplementation implementation : this Node's abstract implementation
	 * @param TileType compute, in : the types of tiles this Node can deploy
	 * @param Map<TileType, List<Node>> lookup : the lookup map
	 * Creates a new Node
	 */
//	public Node(String name, AbstractImplementation implementation, TileType compute, TileType in, String hostname, int uniqueID, int port) {
//		this(name, implementation);
//		this.compute = compute;
//		this.in = in;
//		tiles = new ArrayList<Tile>();
//		initializeArchitecture(port);
//		address = new NodeAddress(hostname, uniqueID, port);
//	}

	
	/*
 	 * @param String name : this Node's name
	 * @param List<TileType> compute, in : the types of tiles this Node might deploy.
	 *                                     This constructor picks one of each list at random.
	 * @param Map<TileType, List<Node>> lookup : the lookup map
	 * Creates a new Node, randomly choosing among the allowed tile types to deploy.
	 */
	public Node(String name, List<TileType> types, String hostname, int uniqueID, int port, BufferedWriter outFile){
		this(name, types.get((int) (Math.random() * types.size())),
				hostname, uniqueID, port, outFile);
	}
	
	/*
	 * @param String name : this Node's name
	 * @param AbstractImplementation implementation : this Node's abstract implementation
	 * @param List<TileType> compute, in : the types of tiles this Node might deploy.
	 *                                     This constructor picks one of each list at random.
	 * @param Map<TileType, List<Node>> lookup : the lookup map
	 * Creates a new Node, randomly choosing among the allowed tile types to deploy.
	 */
//	public Node(String name, AbstractImplementation implementation, List<TileType> computes, List<TileType> ins, String hostname, int uniqueID, int port) {
//		this(name, implementation, computes.get((int) (Math.random() * computes.size())),
//				ins.get((int) (Math.random() * ins.size())), hostname, uniqueID, port);
//	}
	
//	public SocketDistribution getSocketDistribution() {
//		return sd;
//	}
	
//	public Scaffold getScaffold() {
//		return scaffold;
//	}
	
	/*
	 * @returns this node's architecture
	 */
//	public Architecture getArchitecture() {
//		return architecture;
//	}
	
	public Port getNewPort() {
		Port port = new Port("incoming port for local communication", PrismConstants.REPLY);
		addCompPort(port);
		architecture.add(port);
		port.start();
		return port;
	}

	public NodeAddress getAddress() {
		return address;
	}
	
	/*
	 * @param TileType type : the type of a tile
	 * @returns a Node that deploys the particular type of a tile, chosen at random from 
	 * the lookup map.  
	 */
	private Port lookupNode(TileType type) {
		List<Port> valids = (List<Port>) typeLookup.get(type);
		Port valid;
		do {
			valid = valids.get((int) (Math.random() * valids.size()));
		}
		while (seedPorts.contains(valid));
		return valid;
	}
	
	public void updateNeighbor(TileAddress parentNeighborsAddress, int assemblyID, int direction, int fromInNodeID) {
		// send a message to the parentNeighbor asking for its proper child
		Port destination = addressLookup.get(parentNeighborsAddress.getUniqueID());
		ChildUpdateRequest event = new ChildUpdateRequest(assemblyID, direction, fromInNodeID, parentNeighborsAddress.getPortID(), address);
		send(event, destination);
//		neighbors[i] = parentNeighbors[i].getChild(assemblyID);
	}
	
	/*
	 * This should be the only way to create a new tile.
	 * @param TileType type : the type of a tile
	 * @param int assemblyID : the assemblyID of a tile
	 * @param boolean isSeed : indicates whether the new tile is part of a seed
	 * @throws TileTypeException
	 * @returns a new Tile of the given type and with the given assemblyID, deployed 
	 * on this.  If this cannot deploy nodes of type, then throws a TileTypeException.
	 */
	public Tile deployTile(TileType type, int assemblyID, boolean isSeed) throws TileStyleException {
		if (!(type.equals(type))) 
			throw new TileStyleException("Trying to deploy a tile of an incompatible type");
		Tile tile = new Tile(type, this, assemblyID, isSeed);
		tiles.add(tile);
		tile.setInNodeID(tiles.indexOf(tile));

		if (TileStyleStarter.DEBUG)
			System.out.println("A tile is deploying on node " + this + " assembly id = " + assemblyID);

		if(type.getName().endsWith("checkcheck")) {
			String message = "Found solution at " + System.currentTimeMillis();
			System.out.println(message);
			try {
				if (outFile != null) {
					outFile.write(message);
					outFile.newLine();
					outFile.close();
				}
				Runtime.getRuntime().exec("qdel " + TileStyleStarter.JOB_NUM);
			}
			catch (IOException e) {
				throw new TileStyleException(e.getMessage());
			}
			System.exit(0);
		}
		return tile;
	}

	/*
	 * @param Tile original : a tile to replicate
	 * Replicates a tile by finding 2 children.  If original is not ready to 
	 * replicate, throws a TileTypeException.
	 */
	private void replicate(Tile original) throws TileStyleException {

//		if (original.flag)
//			throw new TileStyleException("Already replicated and doing it again!");
//		original.flag = true;

//		System.out.println("replicating");
		
		if (!(original.isReadyToReplicate()))
			throw new TileStyleException("Tile " + original + " is trying to replicate without being ready");
		
		// find new hosts
		Port[] copyHostPorts = new Port[Tile.NUMBER_OF_CHILDREN];
		for (int i = 0; i < Tile.NUMBER_OF_CHILDREN; i++)
			copyHostPorts[i] = lookupNode(original.type);
		
		// get original neighbors
		TileAddress[] neighbors = new TileAddress[4];
		for (int i = 0; i < 4; i++)
			neighbors[i] = original.getNeighbor(i);
		
		//figure out the base for new assemblyIDs
		int baseAssemblyID;
		if (Tile.NUMBER_OF_CHILDREN == 1)
			baseAssemblyID = original.getAssemblyID() + 1;
		else
			baseAssemblyID = original.getAssemblyID() * Tile.NUMBER_OF_CHILDREN;
		
		// create a replicate on each of the new hosts
		for (int i = 0; i < Tile.NUMBER_OF_CHILDREN; i++) {
			send(new CreateReplicateRequest(type, baseAssemblyID + i, neighbors, original.getInNodeID(), address), copyHostPorts[i]);			
		}
	}
	
	/*
	 * @param Tile corner : the tile to the south east of an empty location
	 * @throws TileTypeException
	 * Executes the recruitment procedure.  Throws a TileTypeException if corner is not 
	 * ready to recruit.  Otherwise, finds a new tile, at random from the lookup map that
	 * deploys tiles that can attach to the north west of corner.    
	 */
	public void recruit(Tile corner) throws TileStyleException {
//		System.out.println("recruiting tile: " + corner + " on node: " + this + " ID: " + corner.getAssemblyID());

		if (!(corner.isReadyToRecruit()))
			throw new TileStyleException("Tile " + corner + " is trying to recruit without being ready");

		TileAddress north = corner.getNeighbor(TileType.NORTH);
		TileAddress west = corner.getNeighbor(TileType.WEST);
		Port destination = addressLookup.get(north.getUniqueID());
		
		send(new GetNorthsSideEvent(north, west, corner.getAddress()), destination);
	}
	
	public synchronized void handle(Event event) {
		if (TileStyleStarter.DEBUG)
			System.out.println("handling event of type " + event.getClass());
		if (event instanceof ChildUpdateRequest) {
			ChildUpdateRequest e = (ChildUpdateRequest) event;
			
			Tile tile = tiles.get(e.getToInNodeID());
			
			int childNum;
			if (Tile.NUMBER_OF_CHILDREN == 1)
				childNum = e.getAssemblyID() - tile.getAssemblyID() - 1;
			else
				childNum = e.getAssemblyID() - (tile.getAssemblyID() * Tile.NUMBER_OF_CHILDREN);
			
			if ((childNum >= 0 ) || (childNum < Tile.NUMBER_OF_CHILDREN)) {
				TileAddress childAddress = tile.getChild(childNum);
				ChildUpdateResponse response = new ChildUpdateResponse(childAddress, e.getDirection(), e.getAssemblyID(), e.getFromInNodeID());
				if (childAddress != null)
					send(response, addressLookup.get(e.getReturnAddress().getUniqueID()));
				else {
					// store the info necessary to be sent back until the child is created
					ChildUpdateResponsePacket packet = new ChildUpdateResponsePacket(response, addressLookup.get(e.getReturnAddress().getUniqueID()), childNum);
					tile.addChildUpdateResponsePacket(packet);
				}
			}
			else
				throw new TileStyleException("tried to update a BAD assembly ID");
		}
		else if (event instanceof ChildUpdateResponse) {
			ChildUpdateResponse e = (ChildUpdateResponse) event;
			Tile tile = tiles.get(e.getToInNodeID());
			
			if (!((e.getAddress() == null) && (tile.getNeighbor(e.getDirection()) != null)))
				if (e.getAssemblyID() == tile.getAssemblyID())
					if (e.getAddress() != null)
						tile.setNeighbor(e.getAddress(), e.getDirection());
					else
						if (tile.getStatus() > Tile.UPDATING)
							throw new TileStyleException("Got a null neighbor update AFTER declaring done updating");
						else
							tile.setStatus(Tile.UNREPLICATED);
				else 
					throw new TileStyleException("tried to send back a BAD assembly ID child");
			// else the neighbor has already been set but a more recent event and we don't want to erase it
		}
		else if (event instanceof CreateReplicateRequest) {
			CreateReplicateRequest e = (CreateReplicateRequest) event;
			
//			if (!(in.equals(e.getType()))) {
//				System.out.println("tried to replicate onto a node of a different type");
//				System.exit(1);
//			}

			Tile newborn = null;
			//try {
			newborn = deployTile(e.getType(), e.getAssemblyID(), true);
			//} catch (TileTypeException exception) {
			//	System.out.flush();
			//	System.exit(1);
			//}
			
			TileAddress[] parentNeighbors = e.getNeighbors();
			for (int i = 0; i < 4; i++)
				newborn.setParentNeighbor(parentNeighbors[i], i);
			send(new CreateReplicateReply(newborn.getAddress(), e.getAssemblyID(), e.getFromInNodeID()), addressLookup.get(e.getReturnAddress().getUniqueID()));
		}
		else if (event instanceof CreateReplicateReply) {
			CreateReplicateReply e = (CreateReplicateReply) event;
			
			Tile tile = tiles.get(e.getToInNodeID());
			int childNum;
			if (Tile.NUMBER_OF_CHILDREN == 1)
				childNum = e.getAssemblyID() - tile.getAssemblyID() - 1;
			else
				childNum = e.getAssemblyID() - (tile.getAssemblyID() * Tile.NUMBER_OF_CHILDREN);
			
			if ((childNum >= 0 ) || (childNum < Tile.NUMBER_OF_CHILDREN))
				//try {
				tile.setChild(e.getAddress(), childNum);
				//} catch (TileTypeException exception) {
				//	System.out.flush();
				//	System.exit(1);
				//}
			else
				throw new TileStyleException("tried to update a BAD assembly ID");
		}
		else if (event instanceof GetNorthsSideEvent) {
			GetNorthsSideEvent e = (GetNorthsSideEvent) event;
			
			Port destination = addressLookup.get(e.getWestAddress().getUniqueID());
			Tile tile = tiles.get(e.getNorthAddress().getPortID());
			send(new GetWestsSideEvent(e.getNorthAddress(), e.getWestAddress(), e.getHomeAddress(), tile.getType().getWest()), destination);
		}
		else if (event instanceof GetWestsSideEvent) {
			GetWestsSideEvent e = (GetWestsSideEvent) event;
			
			Port destination = addressLookup.get(e.getHomeAddress().getUniqueID());
			Tile tile = tiles.get(e.getWestAddress().getPortID());
			send(new GotSidesEvent(e.getNorthAddress(), e.getWestAddress(), e.getHomeAddress(), e.getNorthsSide(), tile.getType().getNorth()), destination);			
	}
		else if (event instanceof GotSidesEvent) {
			GotSidesEvent e = (GotSidesEvent) event;
			
			Tile corner = tiles.get(e.getHomeAddress().getPortID());			
//			Tile northNeighborTile = corner.getNeighbor(TileType.NORTH);
//			Tile westNeighborTile = corner.getNeighbor(TileType.WEST);
//			int assemblyID = corner.getAssemblyID();
			
//			if ((northNeighborTile.getNeighbor(TileType.WEST) != null) ||
//				(westNeighborTile.getNeighbor(TileType.NORTH) != null))
//				throw new TileTypeException("Tile " + corner + " is trying to recruit to a filled location");
			
			TileType type;
			synchronized(typeLookup) {
				type = TileType.getNorthWestMatch(typeLookup.keySet().iterator(), 
												  e.getNorthsSide(), e.getWestsSide());
			}
			
			if (type != null) {
				Port attachmentNodePort = lookupNode(type);
				send(new AttachTileEvent(e.getNorthAddress(), e.getWestAddress(), corner.getAssemblyID(), type), attachmentNodePort);
			}
			// else there is no matching tile
			corner.setStatus(Tile.RECRUITED);
		}
		else if (event instanceof AttachTileEvent) {
			AttachTileEvent e = (AttachTileEvent) event;

			Tile newborn = null;
			//try {
			newborn = deployTile(e.getType(), e.getAssemblyID(), false);
			//} catch (TileTypeException e1) {
			//	System.out.flush();
			//	System.exit(1);
			//}
			newborn.setNeighbor(e.getNorthAddress(), TileType.EAST);
			newborn.setNeighbor(e.getWestAddress(), TileType.SOUTH);
			TileAddress newbornAddress = new TileAddress(address.getHostname(), address.getUniqueID(), address.getPort(), tiles.indexOf(newborn));

			Port south = addressLookup.get(e.getWestAddress().getUniqueID());
			Port east = addressLookup.get(e.getNorthAddress().getUniqueID());

			send(new SetNeighborEvent(newbornAddress, e.getWestAddress(), TileType.NORTH), south);
			send(new SetNeighborEvent(newbornAddress, e.getNorthAddress(), TileType.WEST), east);
		}
		else if (event instanceof SetNeighborEvent) {
			SetNeighborEvent e = (SetNeighborEvent) event;

			Tile tile = tiles.get(e.getAddress().getPortID());
			tile.setNeighbor(e.getNewbornAddress(), e.getDirection());
		}
		else if (event instanceof KeepAlive) {
			// do nothing
		}
		else throw new TileStyleException("Unknown event type" + event.getClass());
	}

	/*
	 * @throws TileTypeException
	 * Executes the main method of this.  The node's job is to keep track of all its 
	 * tiles, replicating and recruiting as necessary, whenever the tiles are ready.  
	 */
	public synchronized void execute() throws TileStyleException {
//		System.out.println("I'm node " + this + " and I am executing");
		// Do not use iterator here because tiles can change in the middle of this loop.
		for (int i = 0; i < tiles.size(); i++) {
			Tile tile = tiles.get(i);
			if (tile != null)
				switch(tile.getStatus()) {
				case Tile.UNREPLICATED:
					tile.updateNeighbors();
					tile.setStatus(Tile.UPDATING);
					break;
				case Tile.UPDATING:
					if (tile.isReadyToReplicate()) {
						tile.setStatus(Tile.READY_TO_REPLICATE);
//						System.out.println(tile);
					}
					break;
				case Tile.READY_TO_REPLICATE:
					if(tile.isReadyToReplicate()) {
						replicate(tile);
						tile.setStatus(Tile.REPLICATED);
					}
					else 
						throw new TileStyleException("Tried to replicate when I wan't ready.");
					break;
				case Tile.REPLICATED:
					if (tile.isReadyToRecruit()) {
						recruit(tile);
						tile.setStatus(Tile.RECRUITING);
					}
					break;
				case Tile.RECRUITED:
				break;
				}
		}
	}
}
